package VideoProcessing;

import java.awt.Dimension;
import java.awt.image.BufferedImage;
import java.awt.image.WritableRaster;
import java.beans.PropertyChangeSupport;
import java.util.ArrayList;
import java.util.List;

import javax.media.Buffer;
import javax.media.Effect;
import javax.media.Format;
import javax.media.PlugIn;
import javax.media.format.RGBFormat;
import javax.media.format.VideoFormat;

public abstract class RgbVideoEffect implements Effect {

	public static final String PROP_VIDEO_SIZE = "videoSize";

	protected BufferedImage displayImage;

	private List<VideoFrameListener> frameListenerList;

	// We pretend that our effects will always input and output RGB
	protected Format supportedIns[] = new Format[] { new RGBFormat() };
	protected Format supportedOuts[] = new Format[] { new RGBFormat() };

	private Format input = null, output = null;

	/** For profiling: total processing time ever. */
	private double totalTime;

	/** For profiling: total number of calls to processRGB(). */
	private long nCalls;

	/** Whether or not the effect is active. */
	private boolean active;

	/** Size of the incoming video frames. */
	private Dimension videoSize = new Dimension();

	private PropertyChangeSupport propSupport = new PropertyChangeSupport(this);

	public RgbVideoEffect() {
		active = true;
		displayImage = new BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB);
		frameListenerList = new ArrayList<VideoFrameListener>();
	}

	public Dimension getVideoSize() {
		return videoSize;
	}

	/*
	 * public int process(Buffer in, Buffer out) {
	 * 
	 * if (in.getFormat() instanceof VideoFormat && in.getData() != null) {
	 * byte[] bin; byte[] bout; if (in.getData() instanceof byte[]) { bin =
	 * (byte[]) in.getData(); } else if (in.getData() instanceof int[]) { int[]
	 * iin = (int[]) in.getData(); bin = new byte[iin.length * 3]; int bi, ii;
	 * for (bi = 0, ii = 0; bi < bin.length; bi += 3, ii++) { int v = iin[ii];
	 * bin[bi + 2] = (byte) (v & 0xff); bin[bi + 1] = (byte) ((v >> 8) & 0xff);
	 * bin[bi] = (byte) ((v >> 16) & 0xff); } } else { return
	 * PlugIn.BUFFER_PROCESSED_FAILED; } if (!(out.getData() instanceof byte[])
	 * || ((byte[])out.getData()).length < bin.length) { bout = new
	 * byte[bin.length]; out.setData(bout); } else { bout = (byte[])
	 * out.getData(); }
	 * 
	 * VideoFormat vformat = (VideoFormat) in.getFormat(); if
	 * (vformat.getSize().width != videoSize.width || vformat.getSize().height
	 * != videoSize.height) { videoSize = vformat.getSize();
	 * propSupport.firePropertyChange(PROP_VIDEO_SIZE, null, videoSize); }
	 * 
	 * //// Assure output buffer is large enough if(bout == null || bout.length
	 * < bin.length) { }
	 * 
	 * byte[] buffToDraw = bout; boolean processed = false; if (active) { long
	 * startTime = System.nanoTime();
	 * //System.out.println("in "+this.getClass().getName()); processed =
	 * processRGB(bin, bout, vformat); long stopTime = System.nanoTime();
	 * totalTime += (stopTime - startTime) / 1.0e9; ++nCalls; }
	 * 
	 * // Why swap input and output data? if(!processed) { // Swap the data
	 * between the input & output. Object data = in.getData();
	 * in.setData(out.getData()); // not necessary out.setData(data); buffToDraw
	 * = bin; }
	 * 
	 * //// Update frame image available to UI if (frameListenerList.size() > 0)
	 * {
	 * 
	 * //// Assure the image is the proper size if (displayImage.getWidth() !=
	 * vformat.getSize().width || displayImage.getHeight() !=
	 * vformat.getSize().height) { displayImage = new
	 * BufferedImage(vformat.getSize().width, vformat.getSize().height,
	 * BufferedImage.TYPE_INT_RGB); } updateImage(buffToDraw, vformat);
	 * notifyVideoFrameListeners(); } }
	 * 
	 * // Copy the input attributes to the output
	 * //out.setFormat(in.getFormat()); //out.setLength(in.getLength());
	 * //out.setOffset(in.getOffset());
	 * 
	 * return BUFFER_PROCESSED_OK; }
	 */

	public int process(Buffer in, Buffer out) {
		if (!(in.getFormat() instanceof VideoFormat && in.getData() != null)) {
			return PlugIn.BUFFER_PROCESSED_FAILED;
		}

		byte[] bin, bout;
		VideoFormat vformat = (VideoFormat) in.getFormat();

		// get input byte array
		if (in.getData() instanceof byte[]) {
			bin = (byte[]) in.getData();
		} else if (in.getData() instanceof int[]) {
			int[] rgb = (int[]) in.getData();
			bin = new byte[rgb.length * 3];
			for (int i = 0; i < rgb.length; i++) {
				int index = i * 3;
				int pixel = rgb[i];
				bin[index + 2] = (byte) (pixel & 0xff);
				bin[index + 1] = (byte) (pixel >> 8 & 0xff);
				bin[index] = (byte) (pixel >> 16 & 0xff);
			}
		} else {
			return PlugIn.BUFFER_PROCESSED_FAILED;
		}

		// get output byte array
		if (!(out.getData() instanceof byte[])
				|| ((byte[]) out.getData()).length < bin.length) {
			bout = new byte[bin.length];
			out.setData(bout);
		} else {
			bout = (byte[]) out.getData();
		}

		maintainSize(vformat);

		boolean processed = false;
		if (active) {
			processed = processRGB(bin, bout, vformat);
		}
		byte[] buffToDraw = bout;
		if (!processed) {
			Object data = in.getData();
			out.setData(data);
			buffToDraw = bin;
		}

		updateDisplay(vformat, buffToDraw);
		return BUFFER_PROCESSED_OK;
	}

	private void updateDisplay(VideoFormat vformat, byte[] buffToDraw) {
		if (frameListenerList.size() > 0) {
			boolean isSameSize = displayImage.getWidth() == vformat.getSize().width
					&& displayImage.getHeight() == vformat.getSize().height;
			if (!isSameSize) {
				displayImage = new BufferedImage(vformat.getSize().width,
						vformat.getSize().height, BufferedImage.TYPE_INT_RGB);
			}
			updateImage(buffToDraw, vformat);
			notifyVideoFrameListeners();
		}
	}

	private void maintainSize(VideoFormat vformat) {
		if (!vformat.getSize().equals(videoSize)) {
			videoSize = vformat.getSize();
			propSupport.firePropertyChange(PROP_VIDEO_SIZE, null, videoSize);
		}
	}

	protected abstract boolean processRGB(byte[] bin, byte[] bout,
			VideoFormat format);

	protected void updateImage(byte[] bout, VideoFormat vformat) {
		synchronized (displayImage) {
			WritableRaster rast = displayImage.getRaster();
			int[] pixel = new int[] { 0, 0, 0, 255 };
			int p = 0;
			for (int y = vformat.getSize().height - 1; y >= 0; y--) {
				for (int x = 0; x < vformat.getSize().width; x++) {
					pixel[0] = bout[p++];
					pixel[1] = bout[p++];
					pixel[2] = bout[p++];
					rast.setPixel(x, y, pixel);
				}
			}
		}
	}

	/*
	 * public void setDisplayImage(BufferedImage image) { displayImage = image;
	 * }
	 */
	public BufferedImage getDisplayImage() {
		return displayImage;
	}

	public void addVideoFrameListener(VideoFrameListener listener) {
		synchronized (frameListenerList) {
			frameListenerList.add(listener);
		}
	}

	public void removeVideoFrameListener(VideoFrameListener listener) {
		synchronized (frameListenerList) {
			frameListenerList.remove(listener);
			/*
			 * if(frameListenerList.size() < 0) { displayImage = new
			 * BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB); }
			 */
		}
	}

	/**
	 * Gets the average time per frame that this effect takes to process, in
	 * seconds.
	 * 
	 * @return Average time in seconds.
	 */
	public double getAvgProcessingTime() {
		return totalTime == 0.0 ? 0.0 : (double) nCalls / totalTime;
	}

	public Object[] getControls() {
		return new Object[0];
	}

	public Object getControl(String type) {
		return null;
	}

	public abstract String getName();

	// No op.
	public void open() {
	}

	// No op.
	public void close() {
	}

	// No op.
	public void reset() {
	}

	public Format[] getSupportedInputFormats() {
		// return new Format[] {
		// new RGBFormat(new Dimension(640, 480), 640 * 480 * 4, Byte.class,
		// 29.9f, 8, 1, 2, 3)
		// };
		return supportedIns;
	}

	public Format[] getSupportedOutputFormats(Format in) {
		if (in == null)
			return supportedOuts;
		else {
			// If an input format is given, we use that input format
			// as the output since we are not modifying the bit stream
			// at all.
			Format outs[] = new Format[1];
			outs[0] = in;
			return outs;
		}
	}
	
	protected int byte2Int(byte b) {
		if (b<0) {
			return 256+b;
		} else {
			return b;
		}
	}

	public Format setInputFormat(Format format) {
		input = format;
		return input;
	}

	public Format setOutputFormat(Format format) {
		output = format;
		return output;
	}

	/**
	 * Returns true if the effect is active; false if deactivated.
	 * 
	 * @return true if the effect is active; false if deactivated.
	 */
	public boolean isActive() {
		return active;
	}

	/**
	 * Actives or deactivates this effect. If deactivated, the effect will
	 * simply pass the video frames through to the next effect in the processing
	 * chain.
	 * 
	 * @param active
	 *            True if the effect is active.
	 */
	public void setActive(boolean active) {
		this.active = active;
	}

	/** Notifies the frame listeners of a new frame. */
	private void notifyVideoFrameListeners() {
		synchronized (frameListenerList) {
			for (VideoFrameListener listener : frameListenerList) {
				listener.newVideoFrame(displayImage);
			}
		}
	}
}
